前言

标题是‘从零开始实现一个简易的Java MVC框架’,结果写了这么多才到实现MVC的时候...只能说前戏确实有点多了。不过这些前戏都是必须的,如果只是简简单单实现一个MVC的功能那就没有意思了,要有Bean容器、IOC、AOP和MVC才像是一个'框架'嘛。

实现准备

为了实现mvc的功能,先要为pom.xml添加一些依赖。

<properties>
    ...
    <tomcat.version>8.5.31</tomcat.version>
    <jstl.version>1.2</jstl.version>
    <fastjson.version>1.2.47</fastjson.version>
</properties>
<dependencies>
    ...
    <!-- tomcat embed -->
    <dependency>
        <groupId>org.apache.tomcat.embed</groupId>
        <artifactId>tomcat-embed-jasper</artifactId>
        <version>${tomcat.version}</version>
    </dependency>

    <!-- JSTL -->
    <dependency>
        <groupId>javax.servlet</groupId>
        <artifactId>jstl</artifactId>
        <version>${jstl.version}</version>
        <scope>runtime</scope>
    </dependency>

    <!-- FastJson -->
    <dependency>
        <groupId>com.alibaba</groupId>
        <artifactId>fastjson</artifactId>
        <version>${fastjson.version}</version>
    </dependency>
</dependencies>
  • tomcat-embed-jasper这个依赖是引入了一个内置的tomcat,spring-boot默认就是引用这个嵌入式的tomcat包实现直接启动服务的。这个包除了加入了一个嵌入式的tomcat,还引入了java.servlet-apijsp-api这两个包,如果不想用这种嵌入式的tomcat的话,可以去除tomcat-embed-jasper然后引入这两个包。
  • jstl用于解析jsp表达式的,比如在jsp页面编写下面这样c:forEach语句就需要这个包。

    <c:forEach items="${list}" var="user">
        <tr>
            <td>${user.id}</td>
            <td>${user.name}</td>
        </tr>
    </c:forEach>
  • fastjson是阿里开发的一个json解析包,用于将实体类转换成json。类似的包还有GsonJackson等,这里就不具体比较了,可以挑选一个自己喜欢的。

实现MVC

MVC实现原理

首先我们要了解到MVC的实现原理,在使用spring-boot编写项目的时候,我们通常都是通过编写一系列的Controller来实现一个个链接,这是'现代'的写法。但是在以前springmvc甚至是struts2这类mvc框架都还没流行的时候,都是通过编写Servlet 来实现。

每一个请求都会对应一个Servlet ,然后还要在web.xml中配置这个Servlet ,然后对请求的接收和处理啥的都分布在一大堆的Servlet 中,代码十分混杂。

为了让人们编写的时候更专注于业务代码而减少对请求的处理,springmvc就通过一个中央的Servlet ,处理这些请求,然后再转发到对应的Controller中,这样就只有一个Servlet 统一处理请求了。下面的一段话来自spring的官方文档https://docs.spring.io/spring/docs/5.0.7.RELEASE/spring-framework-reference/web.html#mvc-servlet

Spring MVC, like many other web frameworks, is designed around the front controller pattern where a central Servlet, the DispatcherServlet, provides a shared algorithm for request processing while actual work is performed by configurable, delegate components. This model is flexible and supports diverse workflows.

The DispatcherServlet, as any Servlet, needs to be declared and mapped according to the Servlet specification using Java configuration or in web.xml. In turn the DispatcherServlet uses Spring configuration to discover the delegate components it needs for request mapping, view resolution, exception handling, and more.

这段大致意思就是:springmvc通过中心Servlet(DispatcherServlet)来实现对控制controller的操作。这个Servlet要通过java配置或者配置在web.xml中,它用于寻找请求的映射(即找到对应的controller),视图解析(即执行controller的结果),异常处理(即对执行过程的异常统一处理)等等

所以实现MVC的效果就是以下几点:

  1. 通过一个中央sevlet如DispatcherServlet来接收所有请求
  2. 根据请求找到对应的controller
  3. 执行controller获取结果
  4. 对controller的结果解析并转到对应视图
  5. 若有异常则统一处理异常

根据上面的步骤,我们先从步骤2、3、4、5开始,最后再实现1完成mvc。

创建注解

为了方便实现,先在com.zbw.mvc.annotation包下创建三个注解和一个枚举:RequestMappingRequestParamResponseBodyRequestMethod

package com.zbw.mvc.annotation;
import ...

/**
 * http请求路径
 */
@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
public @interface RequestMapping {
    /**
     * 请求路径
     */
    String value() default "";

    /**
     * 请求方法
     */
    RequestMethod method() default RequestMethod.GET;
}
package com.zbw.mvc.annotation;

/**
 * http请求类型
 */
public enum RequestMethod {
    GET, POST
}
package com.zbw.mvc.annotation;
import ...

/**
 * 请求的方法参数名
 */
@Target(ElementType.PARAMETER)
@Retention(RetentionPolicy.RUNTIME)
public @interface RequestParam {
    /**
     * 方法参数别名
     */
    String value() default "";

    /**
     * 是否必传
     */
    boolean required() default true;
}
package com.zbw.mvc.annotation;
import ...

/**
 * 用于标记返回json
 */
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface ResponseBody {
}

这几个类的作用就不解释了,都是springmvc最常见的注解。

创建ModelAndView

为了能够方便的传递参数到前端,创建一个工具bean,相当于spring中简化版的ModelAndView。这个类创建于com.zbw.mvc.bean包下

package com.zbw.mvc.bean;
import ...

/**
 * ModelAndView
 */
public class ModelAndView {

    /**
     * 页面路径
     */
    private String view;

    /**
     * 页面data数据
     */
    private Map<String, Object> model = new HashMap<>();

    public ModelAndView setView(String view) {
        this.view = view;
        return this;
    }
    public String getView() {
        return view;
    }
    public ModelAndView addObject(String attributeName, Object attributeValue) {
        model.put(attributeName, attributeValue);
        return this;
    }
    public ModelAndView addAllObjects(Map<String, ?> modelMap) {
        model.putAll(modelMap);
        return this;
    }
    public Map<String, Object> getModel() {
        return model;
    }
}

实现Controller分发器

Controller分发器类似于Bean容器,只不过后者是存放Bean的而前者是存放Controller的,然后根据一些条件可以简单的获取对应的Controller。

先在com.zbw.mvc包下创建一个ControllerInfo类,用于存放Controller的一些信息。

package com.zbw.mvc;
import ...

/**
 * ControllerInfo 存储Controller相关信息
 */
@Data
@AllArgsConstructor
@NoArgsConstructor
public class ControllerInfo {
    /**
     * controller类
     */
    private Class<?> controllerClass;

    /**
     * 执行的方法
     */
    private Method invokeMethod;

    /**
     * 方法参数别名对应参数类型
     */
    private Map<String, Class<?>> methodParameter;
}

然后再创建一个PathInfo类,用于存放请求路径和请求方法类型

package com.zbw.mvc;
import ...

/**
 * PathInfo 存储http相关信息
 */
@Data
@AllArgsConstructor
@NoArgsConstructor
public class PathInfo {
    /**
     * http请求方法
     */
    private String httpMethod;

    /**
     * http请求路径
     */
    private String httpPath;
}

接着创建Controller分发器类ControllerHandler

package com.zbw.mvc;
import ...

/**
 * Controller 分发器
 */
@Slf4j
public class ControllerHandler {

    private Map<PathInfo, ControllerInfo> pathControllerMap = new ConcurrentHashMap<>();

    private BeanContainer beanContainer;

    public ControllerHandler() {
        beanContainer = BeanContainer.getInstance();
        Set<Class<?>> classSet = beanContainer.getClassesByAnnotation(RequestMapping.class);
        for (Class<?> clz : classSet) {
            putPathController(clz);
        }
    }

    /**
     * 获取ControllerInfo
     */
    public ControllerInfo getController(String requestMethod, String requestPath) {
        PathInfo pathInfo = new PathInfo(requestMethod, requestPath);
        return pathControllerMap.get(pathInfo);
    }

    /**
     * 添加信息到requestControllerMap中
     */
    private void putPathController(Class<?> clz) {
        RequestMapping controllerRequest = clz.getAnnotation(RequestMapping.class);
        String basePath = controllerRequest.value();
        Method[] controllerMethods = clz.getDeclaredMethods();
        // 1. 遍历Controller中的方法
        for (Method method : controllerMethods) {
            if (method.isAnnotationPresent(RequestMapping.class)) {
                // 2. 获取这个方法的参数名字和参数类型
                Map<String, Class<?>> params = new HashMap<>();
                for (Parameter methodParam : method.getParameters()) {
                    RequestParam requestParam = methodParam.getAnnotation(RequestParam.class);
                    if (null == requestParam) {
                        throw new RuntimeException("必须有RequestParam指定的参数名");
                    }
                    params.put(requestParam.value(), methodParam.getType());
                }
                // 3. 获取这个方法上的RequestMapping注解
                RequestMapping methodRequest = method.getAnnotation(RequestMapping.class);
                String methodPath = methodRequest.value();
                RequestMethod requestMethod = methodRequest.method();
                PathInfo pathInfo = new PathInfo(requestMethod.toString(), basePath + methodPath);
                if (pathControllerMap.containsKey(pathInfo)) {
                    log.error("url:{} 重复注册", pathInfo.getHttpPath());
                    throw new RuntimeException("url重复注册");
                }
                // 4. 生成ControllerInfo并存入Map中
                ControllerInfo controllerInfo = new ControllerInfo(clz, method, params);
                this.pathControllerMap.put(pathInfo, controllerInfo);
                log.info("Add Controller RequestMethod:{}, RequestPath:{}, Controller:{}, Method:{}",
                        pathInfo.getHttpMethod(), pathInfo.getHttpPath(),
                        controllerInfo.getControllerClass().getName(), controllerInfo.getInvokeMethod().getName());
            }
        }
    }
}

这个类最复杂的就是构造函数中调用的putPathController()方法,这个方法也是这个类的核心方法,实现了controller类中的信息存放到pathControllerMap变量中的功能。大概讲解一些这个类的功能流程:

  1. 在构造方法中获取Bean容器BeanContainer的单例实例
  2. 获取并遍历BeanContainer中存放的被RequestMapping注解标记的类
  3. 遍历这个类中的方法,找出被RequestMapping注解标记的方法
  4. 获取这个方法的参数名字和参数类型,生成ControllerInfo
  5. 根据RequestMapping里的value()method()生成PathInfo
  6. 将生成的PathInfoControllerInfo存到变量pathControllerMap
  7. 其他类通过调用getController()方法获取到对应的controller

以上就是这个类的流程,其中有个注意的点:

步骤4的时候,必须规定这个方法的所有参数名字都被RequestParam注解标注,这是因为在java中,虽然我们编写代码的时候是有参数名的,比如String name这样的形式,但是被编译成class文件后‘name’这个字段就会被擦除,所以必须要通过一个RequestParam来保存名字。

但是大家在springmvc中并不用必须每个方法都用注解标记的,这是因为spring中借助了asm ,这种工具可以在编译之前拿到参数名然后保存起来。还有一种方法是在java8之后支持了保存参数名,但是必须修改编译器的参数来支持。这两种方法实现起来都比较复杂或者有限制条件,这里就不实现了,大家可以查找资料自己实现

实现结果执行器

接下来实现结果执行器,这个类中实现刚才mvc流程中的步骤3、4、5。

在com.zbw.mvc包下创建类ResultRender

package com.zbw.mvc;
import ...

/**
 * 结果执行器
 */
@Slf4j
public class ResultRender {

    private BeanContainer beanContainer;

    public ResultRender() {
        beanContainer = BeanContainer.getInstance();
    }

    /**
     * 执行Controller的方法
     */
    public void invokeController(HttpServletRequest req, HttpServletResponse resp, ControllerInfo controllerInfo) {
        // 1. 获取HttpServletRequest所有参数
        Map<String, String> requestParam = getRequestParams(req);
        // 2. 实例化调用方法要传入的参数值
        List<Object> methodParams = instantiateMethodArgs(controllerInfo.getMethodParameter(), requestParam);

        Object controller = beanContainer.getBean(controllerInfo.getControllerClass());
        Method invokeMethod = controllerInfo.getInvokeMethod();
        invokeMethod.setAccessible(true);
        Object result;
        // 3. 通过反射调用方法
        try {
            if (methodParams.size() == 0) {
                result = invokeMethod.invoke(controller);
            } else {
                result = invokeMethod.invoke(controller, methodParams.toArray());
            }
        } catch (Exception e) {
            throw new RuntimeException(e);
        }
        // 4.解析方法的返回值,选择返回页面或者json
        resultResolver(controllerInfo, result, req, resp);
    }

    /**
     * 获取http中的参数
     */
    private Map<String, String> getRequestParams(HttpServletRequest request) {
        Map<String, String> paramMap = new HashMap<>();
        //GET和POST方法是这样获取请求参数的
        request.getParameterMap().forEach((paramName, paramsValues) -> {
            if (ValidateUtil.isNotEmpty(paramsValues)) {
                paramMap.put(paramName, paramsValues[0]);
            }
        });
        // TODO: Body、Path、Header等方式的请求参数获取
        return paramMap;
    }

    /**
     * 实例化方法参数
     */
    private List<Object> instantiateMethodArgs(Map<String, Class<?>> methodParams, Map<String, String> requestParams) {
        return methodParams.keySet().stream().map(paramName -> {
            Class<?> type = methodParams.get(paramName);
            String requestValue = requestParams.get(paramName);
            Object value;
            if (null == requestValue) {
                value = CastUtil.primitiveNull(type);
            } else {
                value = CastUtil.convert(type, requestValue);
                // TODO: 实现非原生类的参数实例化
            }
            return value;
        }).collect(Collectors.toList());
    }


    /**
     * Controller方法执行后返回值解析
     */
    private void resultResolver(ControllerInfo controllerInfo, Object result, HttpServletRequest req, HttpServletResponse resp) {
        if (null == result) {
            return;
        }
        boolean isJson = controllerInfo.getInvokeMethod().isAnnotationPresent(ResponseBody.class);
        if (isJson) {
            // 设置响应头
            resp.setContentType("application/json");
            resp.setCharacterEncoding("UTF-8");
            // 向响应中写入数据
            try (PrintWriter writer = resp.getWriter()) {
                writer.write(JSON.toJSONString(result));
                writer.flush();
            } catch (IOException e) {
                log.error("转发请求失败", e);
                // TODO: 异常统一处理,400等...
            }
        } else {
            String path;
            if (result instanceof ModelAndView) {
                ModelAndView mv = (ModelAndView) result;
                path = mv.getView();
                Map<String, Object> model = mv.getModel();
                if (ValidateUtil.isNotEmpty(model)) {
                    for (Map.Entry<String, Object> entry : model.entrySet()) {
                        req.setAttribute(entry.getKey(), entry.getValue());
                    }
                }
            } else if (result instanceof String) {
                path = (String) result;
            } else {
                throw new RuntimeException("返回类型不合法");
            }
            try {
                req.getRequestDispatcher("/templates/" + path).forward(req, resp);
            } catch (Exception e) {
                log.error("转发请求失败", e);
                // TODO: 异常统一处理,400等...
            }
        }
    }
}

通过调用类中的invokeController()方法反射调用了Controller中的方法并根据结果解析对应的页面。主要流程为:

  1. 调用 getRequestParams() 获取HttpServletRequest中参数
  2. 调用 instantiateMethodArgs() 实例化调用方法要传入的参数值
  3. 通过反射调用目标controller的目标方法
  4. 调用resultResolver()解析方法的返回值,选择返回页面或者json

通过这几个步骤算是凝聚了MVC核心步骤了,不过由于篇幅问题,几乎每一步骤得功能都有所精简,如

  • 步骤1获取HttpServletRequest中参数只获取get或者post传的参数,实际上还有 Body、Path、Header等方式的请求参数获取没有实现
  • 步骤2实例化调用方法的值只实现了java的原生参数,自定义的类的实例化没有实现
  • 步骤4异常统一处理也没具体实现

虽然有缺陷,但是一个MVC流程是完成了。接下来就要把这些功能组装一下了。

实现DispatcherServlet

终于到实现开头说的DispatcherServlet了,这个类继承于HttpServlet,所有请求都从这里经过。

在com.zbw.mvc下创建DispatcherServlet

package com.zbw.mvc;
import ...

/**
 * DispatcherServlet 所有http请求都由此Servlet转发
 */
@Slf4j
public class DispatcherServlet extends HttpServlet {

    private ControllerHandler controllerHandler = new ControllerHandler();

    private ResultRender resultRender = new ResultRender();

    /**
     * 执行请求
     */
    @Override
    protected void service(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        // 设置请求编码方式
        req.setCharacterEncoding("UTF-8");
        //获取请求方法和请求路径
        String requestMethod = req.getMethod();
        String requestPath = req.getPathInfo();
        log.info("[DoodleConfig] {} {}", requestMethod, requestPath);
        if (requestPath.endsWith("/")) {
            requestPath = requestPath.substring(0, requestPath.length() - 1);
        }

        ControllerInfo controllerInfo = controllerHandler.getController(requestMethod, requestPath);
        log.info("{}", controllerInfo);
        if (null == controllerInfo) {
            resp.sendError(HttpServletResponse.SC_NOT_FOUND);
            return;
        }
        resultRender.invokeController(req, resp, controllerInfo);
    }
}

在这个类里调用了ControllerHandlerResultRender两个类,先根据请求的方法和路径获取对应的ControllerInfo,然后再用ControllerInfo解析出对应的视图,然后就能访问到对应的页面或者返回对应的json信息了。

然而一直在说的所有请求都从DispatcherServlet经过好像没有体现啊,这是因为要配置web.xml才行,现在很多都在使用spring-boot的朋友可能不大清楚了,在以前使用springmvc+spring+mybatis时代的时候要写很多配置文件,其中一个就是web.xml,要在里面添加上。通过通配符*让所有请求都走的是DispatcherServlet。

<servlet>
    <servlet-name>springMVC</servlet-name>
    <servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
    <load-on-startup>1</load-on-startup>
    <async-supported>true</async-supported>
</servlet>
<servlet-mapping>
    <servlet-name>springMVC</servlet-name>
    <url-pattern>*</url-pattern>
</servlet-mapping>

不过我们无需这样做,为了致敬spring-boot,我们会在下一节实现内嵌Tomcat,并通过启动器启动。

缺陷

可能这一节的代码让大家看起来不是很舒服,这是因为目前这个代码虽然说功能已经是实现了,但是代码结构还需要优化。

首先DispatcherServlet是一个请求分发器,这里面不应该有处理Http的逻辑代码的

其次我们把MVC步骤的3、4、5的时候都放在了一个类里,这样也不好,本来这里每一步骤的功能就很繁杂,还将这几步骤都放在一个类中,这样不利于后期更改对应步骤的功能。

还有目前也没实现异常的处理,不能返回异常页面给用户。

这些优化工作会在后期的章节完成的。


源码地址:doodle

原文地址:从零开始实现一个简易的Java MVC框架(七)--实现MVC


zzzzbw
922 声望383 粉丝

Playing and Coding